antlr v4入门学习笔记

回忆是一座桥却是通往寂寞的牢‌,每次回望都心如刀绞却忘不掉

Posted by yishuifengxiao on 2026-06-27

环境搭建与第一个程序

环境准备

  • 安装 Java: ANTLR4 基于 Java,需要先安装 Java 运行时环境 (JRE) 或开发工具包 (JDK)。
  • 获取 ANTLR4:
    • 命令行方式: 从官网下载 ANTLR4 的完整 Jar 包。为了方便,可以在系统里配置好 antlr4grun 命令别名。
    • IDE 插件方式 (推荐): 在 IntelliJ IDEAVSCode 中搜索并安装 ANTLR4 插件。插件能提供语法高亮、代码生成、语法图预览等功能,对初学者非常友好。

编写第一个语法文件:Hello.g4

创建一个名为 Hello.g4 的文件,这是的语法定义文件。输入以下内容:

// Define a grammar called Hello
grammar Hello; // ① 语法名称,必须与文件名一致

r : 'hello' ID ; // ② 语法规则:匹配关键字 'hello' 后跟一个标识符

ID : [a-z]+ ; // ③ 词法规则:匹配由小写字母组成的标识符
WS : [ \t\r\n]+ -> skip ; // ④ 词法规则:跳过空格、制表符、换行符

逐行解释:

  • grammar Hello;:定义语法的名字为 Hello文件名必须与此一致Hello.g4)。
  • r : 'hello' ID ;语法规则 (parser rule),描述语言的语法结构。它表示想匹配一个 'hello' 字符串,后面跟着一个标识符 (ID)。
  • ID : [a-z]+ ;词法规则 (lexer rule),用正则表达式定义ID是什么。[a-z]+ 表示一个或多个小写字母。注意,词法规则名通常以大写字母开头
  • WS : [ \t\r\n]+ -> skip ;:另一个词法规则,定义空白字符,并用 -> skip 指令告诉 ANTLR 忽略它。

生成解析器并测试

  • 生成代码:在 IDEA 中,右键点击 Hello.g4 文件,选择 Generate ANTLR Recognizer。ANTLR4 会为生成一套 Java 代码(词法分析器、语法分析器等)。
  • 测试语法:在 Hello.g4 编辑器中,右键点击规则 r 的那一行,选择 Test Rule r。在弹出窗口的 Input 框中输入 hello world,会看到生成的语法分析树,直观展示了解析结果。

ANTLR4 的核心语法

掌握核心语法,就能定义更复杂的语言了。所有规则都写在 .g4 文件中。

语法文件结构

一个典型的语法文件结构如下:

/** 可选的 Javadoc 风格注释 */
grammar Name; // 语法名称,必须和文件名一致

options {...} // 可选:配置选项
import ... ; // 可选:导入其他语法文件

// 词法规则 (通常大写开头)
TOKEN1 : ... ;
TOKEN2 : ... ;

// 语法规则 (通常小写开头)
rule1 : ... ;
rule2 : ... ;

词法规则 (Lexer Rules)

词法规则负责将字符流分解为词法符号 (Token),类似于“单词”。

  • 规则名必须以大写字母开头
  • 规则体使用正则表达式风格定义。
  • 示例:
    NUMBER : [0-9]+ ;         // 匹配数字
    STRING : '"' .*? '"' ; // 匹配双引号字符串
    COMMENT : '//' ~[\r\n]* ; // 匹配单行注释
  • 片段规则 (fragment):可被其他词法规则复用的规则,本身不会成为独立的Token。
    fragment DIGIT : [0-9] ;
    NUMBER : DIGIT+ ; // 引用片段规则 DIGIT

语法规则 (Parser Rules)

语法规则描述语言的语法结构,由词法符号和子规则组成。

  • 规则名必须以小写字母开头
  • 规则体可以包含:
    • 词法符号:如 '+', 'if', ID
    • 子规则:如 expr
    • 操作符
      • | : 备选分支,表示“或”。
      • *, +, ? : 分别表示“零次或多次”、“一次或多次”、“零次或一次”。
      • ( ) : 对子规则分组。
  • 示例 (简单算术表达式):
    expr : expr ('*'|'/') expr   // 乘除
    | expr ('+'|'-') expr // 加减
    | NUMBER // 数字
    ;
    这个规则通过递归顺序定义了运算符的优先级。

高级特性

  • 备选分支标签 (Alternative Labels):用 # 给分支打标签,ANTLR4 会为每个标签生成独立的监听或访问方法,方便精细处理。

    expr: expr '*' expr # Multiply
    | expr '+' expr # Add
    | NUMBER # Number
    ;
  • 动作 (Actions):用 {...} 在规则中嵌入目标语言(如 Java)的代码,用于执行自定义逻辑,如计算值。

    expr returns [int value]
    : NUMBER { $value = Integer.parseInt($NUMBER.text); }
    | e1=expr '+' e2=expr { $value = $e1.value + $e2.value; }
    ;
  • @header@members:在生成的解析器类中,分别用于插入包声明和自定义成员变量/方法。

词法与语法

这个问题非常关键,是理解 ANTLR4 工作方式的核心。为了让直观理解,先打个比方:词法规则处理“单词”(字母组合),语法规则处理“句子”(单词顺序和结构)。两者分工明确,又协同完成解析任务。

它分别起什么作用?(核心职责)

词法规则(Lexer Rules):将字符流变成 Token 流

  • 作用:读入源代码(字符串),根据规则将连续的字符切分并归类为有意义的“词法单元”(Token),并丢弃对语法分析无用的内容(如空格、注释)。
  • ASN.g4 中的实例
    • ASSIGN : '::=' ; :将两个冒号和一个等号组成的字符序列识别为“赋值”Token。
    • UCASE_ID : ('A'..'Z') ... ; :将“MyModule”这样的连续字母识别为“大写标识符”Token。
    • NUMBER : '0' | ('1'..'9')('0'..'9')* ; :将“2026”识别为“数字”Token。
    • WS : [ \t\r\n]+ -> skip ; :匹配空格、换行,并直接丢弃(不出现在后续解析中)。
    • LINE_COMMENT : '--' ~[\r\n]* -> skip ; :匹配 ASN.1 的注释并丢弃。
  • 一句话总结:词法规则负责 “拼写检查”,把原始文本拆成系统认识的单词(Token)。

语法规则(Parser Rules):将 Token 流变成语法树(Parse Tree)

  • 作用:接收词法分析器产生的 Token 流,依据定义的语法结构(顺序、层次、嵌套、递归),将 Token 组合成更大的结构单元,最终生成一颗语法分析树
  • ASN.g4 中的实例
    • specification: moduleDefinition* ; :规定一个文件由“零个或多个模块定义”组成。
    • moduleDefinition: moduleIdentifier DEFINITIONS_LITERAL ... ; :规定一个模块必须先是模块名,然后是 DEFINITIONS 关键字,再接着是其他成分。这里的 DEFINITIONS_LITERAL 就是引用词法规则定义好的 Token。
    • sequenceType: SEQUENCE_LITERAL LCURLY componentTypeLists? RCURLY ; :规定 SEQUENCE { ... } 这种结构,内部的 componentTypeLists 递归定义了嵌套的组件列表。
  • 一句话总结:语法规则负责 “语法检查”,把单词组合成符合语法的句子,并理清它的层级关系。

词法规则与语法规则的区别

对比维度 词法规则 (Lexer Rules) 语法规则 (Parser Rules)
命名规范 必须以大写字母开头(如 INTEGER_LITERAL, WS 必须以小写字母开头(如 moduleDefinition, type
输入数据 处理字符流(一个一个的字符) 处理Token 流(词法分析器输出的结果)
输出产物 输出 Token(给语法分析器用) 输出 语法树(Parse Tree)(给后续应用用)
描述能力 使用正则表达式风格(字符集 [a-z]、量词 +* 使用上下文无关文法风格(引用其他规则、支持递归)
递归支持 不支持左递归,一般只做简单拼接 支持递归(如 type 可以嵌套包含 type),这是解析嵌套结构的关键
处理内容 处理原子性细节:数字、标识符、字符串、注释、空白 处理结构性逻辑:语句块、函数定义、类型声明、表达式层级
丢弃能力 -> skip 命令,可直接丢弃匹配的文本(如空格、注释) 无法丢弃 Token,所有匹配的 Token 都必须参与构建语法树
典型例子 (ASN.g4) BSTRING : '\'' ('0'..'1')* '\'' 'B' ;
(匹配二进制串字面量)
typeAssignment: UCASE_ID ASSIGN type ;
(规定类型赋值的结构)

词法规则与语法规则的共同点

尽管分工不同,但它在很多方面是统一的

  1. 写在同一个 .g4 文件里:可以把词法规则和语法规则混合编写(通常语法规则在上,词法规则在下)。
  2. 使用相同的 EBNF 元语法:两者都使用 ANTLR 提供的操作符来描述重复或选择:
    • | :或(选择)
    • * :零次或多次
    • + :一次或多次
    • ? :零次或一次
    • ( ) :分组
    • 例如:词法规则中的 ('0'..'9')+ 和语法规则中的 assignment* 都使用了 +*
  3. 都能包含动作(Actions):两者都可以嵌入目标语言代码(如 { System.out.println("..."); })来实现副作用(虽然实践中极少在词法规则里写复杂动作)。
  4. 共同生成解析器类:运行 antlr4 ASN.g4 后,ANTLR 会为词法规则生成 ASNLexer.java,为语法规则生成 ASNParser.java,它共同组成完整的解析器。

它在 ASN.g4 中是如何协作的?

以解析 MyType ::= INTEGER 这句 ASN.1 代码为例,两者的协作过程如下:

  1. 词法分析器出场(词法规则工作):
    • 扫描字符串 MyType ::= INTEGER
    • 识别 MyType → 匹配 UCASE_ID 规则,生成 Token UCASE_ID
    • 识别 ::= → 匹配 ASSIGN 规则,生成 Token ASSIGN
    • 识别 INTEGER → 匹配 INTEGER_LITERAL 规则,生成 Token INTEGER_LITERAL
    • (空格被 WS 规则匹配并跳过,不生成 Token)。
  2. 语法分析器出场(语法规则工作):
    • 接收 Token 流:UCASE_IDASSIGNINTEGER_LITERAL
    • 查找语法规则:typeAssignment 规则定义为 ( UCASE_ID | ... ) ASSIGN type
    • 发现 UCASE_ID 匹配左边,ASSIGN 匹配中间,type 规则进而匹配 INTEGER_LITERAL
    • 最终生成一颗以 typeAssignment 为根的语法树。

关键洞察:语法规则完全看不到原始字符,只能看到词法规则包装好的 Token。这种分层设计极大地降低了语法规则的复杂度。


为什么需要分开它

  1. 效率:词法分析采用正则引擎(状态机),处理字符级匹配非常高效;语法分析采用自适应 LL(*) 算法,处理 Token 流更关注结构。混在一起会拖慢速度。
  2. 分离关注点:词法规则负责“识词”的复杂性(如处理转义字符、十六进制),语法规则负责“识句”的复杂性(如处理优先级、嵌套)。分开让代码更清晰、更容易维护。
  3. 复用性:同一个 Token(如 ID)可以被多个语法规则引用,符合 DRY 原则。

词法规则把“字符”拼成“单词”,语法规则把“单词”组成“句子”。它各司其职,通过 Token 流衔接,共同完成从原始文本到结构化语法树的解析全过程。

理解了这一点,就能清晰地分辨出哪些是在定义 ASN.1 的“单词表”(那几百个 _LITERAL 和标识符规则),哪些是在定义 ASN.1 的“语法书”(moduleDefinition, type, value 等规则)。这种分层思维是学习所有编译原理工具的第一步,已经掌握了!

在程序中使用生成的解析器

生成代码后,需要在主程序中调用它。以下是 Java 的典型使用步骤:

import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.*;

public class Main {
public static void main(String[] args) throws Exception {
// 1. 创建输入流
CharStream input = CharStreams.fromString("的输入文本");
// 2. 创建词法分析器,将文本转为Token流
HelloLexer lexer = new HelloLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
// 3. 创建语法分析器,从Token流开始解析
HelloParser parser = new HelloParser(tokens);
// 4. 从起始规则开始解析,得到语法分析树 (Parse Tree)
ParseTree tree = parser.r(); // r 是在Hello.g4里定义的起始规则
// 5. 使用监听器 (Listener) 遍历语法树
ParseTreeWalker walker = new ParseTreeWalker();
walker.walk(new HelloBaseListener(){
// 在这里重写 enter/exit 方法处理节点
}, tree);
}
}

ANTLR4 支持 Java、C#、Python、JavaScript、Go 等多种目标语言。

掌握 Listener (监听器)Visitor (访问器) 两种遍历语法树的方式。Listener 是自动遍历的“推送”模型,而 Visitor 是可以手动控制遍历的“拉取”模型。

查阅文档与社区:遇到问题查阅 ANTLR4 官方文档GitHub 上的语法示例库,并阅读《The Definitive ANTLR 4 Reference》这本权威书籍。

  • 遵循命名规范:语法规则小写开头,词法规则大写开头。
  • 使用标签区分分支:为 # 分支打标签,让代码更清晰。
  • 处理所有输入:在起始规则末尾加上 EOF,确保解析器消费整个输入,避免静默忽略错误。
  • 利用片段规则:用 fragment 组织可复用的词法单元。

理解 ANTLR4 语法文件的结构

一个 .g4 文件由三部分组成:

grammar Name;          // ① 语法名称(必须和文件名一致)

// ② 可选配置(如 import, options 等)

// ③ 词法规则(通常以大写字母开头)
TOKEN1 : ... ;
TOKEN2 : ... ;

// ④ 语法规则(通常以小写字母开头)
rule1 : ... ;
rule2 : ... ;
  • 词法规则:定义单词(Token),描述字符组合(例如标识符、数字、字符串)。使用正则表达式风格。
  • 语法规则:定义句子(结构),描述 Token 如何组成更大的结构。使用类似 BNF 的语法(| 表示“或”,* 表示零次或多次,+ 表示一次或多次,? 表示零次或一次,括号用于分组)。

现在,打开 ASN.g4 文件,对照着看。


初识 ASN.g4 —— 一个真实的 ASN.1 语法

ASN.1(抽象语法标记一)是电信和网络协议中广泛使用的数据描述语言。这个语法文件非常完整,遵循国际标准(ITU-T X.680)。虽然看起来庞大,但只挑选核心部分来理解。

文件头部

grammar ASN;   // 文件名必须为 ASN.g4

后面是版权注释(略)。这里定义了语法名称为 ASN

词法规则示例

文件后半部分定义了所有 Token(关键字、标点、字面量等)。例如:

ASSIGN : '::=' ;          // 赋值符号
LCURLY : '{' ; // 左花括号
RCURLY : '}' ; // 右花括号
INTEGER_LITERAL : 'INTEGER' ; // 关键字 INTEGER
BSTRING : '\'' ('0'..'1')* '\'' 'B' ; // 二进制串,如 '1010'B

注意:词法规则名通常大写开头,而语法规则名小写开头(这是 ANTLR 的强制约定)。

关键点:fragment 规则

fragment EscapeSequence : '\\' ('b'|'t'|'n'|'f'|'r'|'"'|'\''|'\\') ;
CSTRING : '"' ( EscapeSequence | ~('\\'|'"') )* '"' ;
  • fragment 表示 EscapeSequence 本身不会成为独立的 Token,它只被 CSTRING 复用。这样避免重复定义。

语法规则示例

语法规则定义了 ASN.1 的结构。来看最顶层的规则:

specification: moduleDefinition* ;
  • 一个 ASN.1 规范文件由零个或多个模块定义组成(* 表示重复)。

模块定义:

moduleDefinition:  
moduleIdentifier
DEFINITIONS_LITERAL
tagDefault?
extensionDefault?
ASSIGN
BEGIN_LITERAL
( exports? imports? assignment assignment* )?
END_LITERAL
;
  • 它依次匹配:模块标识符 → DEFINITIONS 关键字 → 可选的标签默认值 → 可选的扩展默认值 → ::=BEGIN → 可选的模块体(导出、导入、赋值语句) → END
  • ? 表示可选,* 表示零个或多个。

嵌套结构举例:typeAssignment

typeAssignment:
( UCASE_ID | characterStringType )
ASSIGN
type
;
  • 一个类型赋值,比如 MyType ::= INTEGER。左边可以是类型引用(大写开头)或内建字符串类型(如 IA5String),右边是一个 type 规则。

type 规则(核心)

type: builtinType constraint? | referencedType constraint? ;
  • 一个类型可以是内建类型(如 INTEGER, SEQUENCE)或引用类型(如已经定义的类型名),后面可以跟一个可选的约束(用括号括起来)。

内建类型分支示例:

builtinType:
bitStringType
| BOOLEAN_LITERAL
| characterStringType
| choiceType
| integerType
| sequenceType
| ...
;

每个类型又有自己的详细规则,例如:

sequenceType:
SEQUENCE_LITERAL LCURLY componentTypeLists? RCURLY
;

这匹配 SEQUENCE { ... } 的形式,其中 componentTypeLists 定义了内部组件的列表,支持扩展(...)、可选、默认等。


动手生成解析器并测试

现在,利用 ASN.g4 生成一个解析器,并测试一个简单的 ASN.1 模块。

生成代码

在命令行进入 ASN.g4 所在目录,执行:

antlr4 ASN.g4

这会生成一堆 Java 文件:ASNLexer.java, ASNParser.java, ASNListener.java 等。如果要生成访问器(Visitor),加上 -visitor 选项:

antlr4 -visitor ASN.g4

编写一个测试输入文件 test.asn

创建一个文本文件,内容为一个最简单的 ASN.1 模块:

MyModule DEFINITIONS ::= BEGIN
MyType ::= INTEGER
END

使用 TestRig(grun)可视化解析树

在命令行执行:

grun ASN specification -gui test.asn

(如果使用 grun 别名无效,直接写 java org.antlr.v4.gui.TestRig ASN specification -gui test.asn

这会弹出一个图形窗口,展示解析树(Parse Tree),可以直观看到每个节点是如何匹配的。

注意specification 是入口规则,因为一个文件包含多个模块。也可以从 moduleDefinition 开始测试单个模块,但这里用 specification 更完整。


遍历解析树——监听器(Listener)与访问器(Visitor)

生成解析器后,通常需要遍历语法树来提取信息、验证语义或生成代码。ANTLR4 提供了两种模式:

监听器(Listener)

  • 自动遍历树,在进入/离开每个节点时回调定义的方法。
  • 使用 ParseTreeWalker 驱动。

示例 Java 代码(处理的 test.asn):

import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.*;

public class MyListener extends ASNBaseListener {
@Override
public void enterTypeAssignment(ASNParser.TypeAssignmentContext ctx) {
System.out.println("发现类型赋值: " + ctx.getChild(0).getText());
}
}

public class Main {
public static void main(String[] args) throws Exception {
CharStream input = CharStreams.fromFileName("test.asn");
ASNLexer lexer = new ASNLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
ASNParser parser = new ASNParser(tokens);
ParseTree tree = parser.specification(); // 从顶层规则开始

ParseTreeWalker walker = new ParseTreeWalker();
walker.walk(new MyListener(), tree);
}
}

运行后会打印 “发现类型赋值: MyType”。

访问器(Visitor)

  • 可以显式控制遍历顺序,返回任意值。
  • 需要生成时加 -visitor,然后继承 ASNBaseVisitor<T> 并重写方法。

示例:

public class MyVisitor extends ASNBaseVisitor<String> {
@Override
public String visitTypeAssignment(ASNParser.TypeAssignmentContext ctx) {
String name = ctx.UCASE_ID().getText();
String type = visit(ctx.type()); // 递归访问 type 子节点
return "类型 " + name + " 定义为 " + type;
}
}

然后在主程序调用 tree.accept(new MyVisitor())

选择建议:如果只需被动响应节点,用 Listener;如果需要控制遍历顺序或返回值,用 Visitor。


高级技巧

备选分支标签(Alternative Labels)

value 规则中,看到:

value: CSTRING # value_CSTRING
| BSTRING # value_BSTRING
...

这样生成的监听器/访问器会有专门的方法,如 enterValue_CSTRING,避免在同一个 enterValue 中做类型判断,让代码更清晰。

使用 -> skip 忽略空白和注释

WS : [ \t\r\n]+ -> skip ;
LINE_COMMENT : '--' ~[\r\n]* -> skip ;
BLOCK_COMMENT : '/*' .*? '*/' -> skip ;

这些 Token 被丢弃,不会出现在解析树中,简化了语法规则。

处理歧义

componentTypeLists 中,有多个备选分支,顺序很重要。ANTLR4 按书写顺序尝试匹配,因此要把更具体的分支放前面,避免歧义。

实战示例

文件结构总览

grammar ASN;                   // 1. 语法名称,文件名必须为 ASN.g4
// 2. 版权和说明注释(略)
/*--------------------- Specification ----------*/
specification: moduleDefinition* ;

/*--------------------- Module definition ------*/
moduleDefinition: ... ;
...

/*--------------------- Types ------------------*/
type: builtinType | referencedType ... ;

/*--------------------- Values -----------------*/
value: ... ;

/*--------------------- Constraints ------------*/
constraint: ... ;

/*--------------------- LITERAL (Keywords) -----*/
ANY_LITERAL: 'ANY';
...

/*--------------------- Lexical Items ----------*/
ASSIGN: '::=' ;
BSTRING: ... ;
UCASE_ID: ... ;
WS: ... ;

整体分为:

  1. 语法规则部分(小写开头):定义 ASN.1 的句法结构。
  2. 词法规则部分(大写开头):定义关键字、标点、字面量、标识符、注释等。
  3. 顺序上,语法规则在前,词法规则在后(ANTLR4 允许,但推荐将词法规则置于文件尾部)。

词法规则(Lexer Rules)解析

词法规则负责将字符流切分成 Token,是解析的基础。此文件定义了:

关键字(保留字)

约 50 多个,如 ANY_LITERAL: 'ANY';INTEGER_LITERAL: 'INTEGER';EXPLICIT_LITERAL: 'EXPLICIT'; 等。
命名习惯:在保留字后加 _LITERAL 后缀,避免与语法规则名冲突,且便于识别。

标点符号

  • ASSIGN: '::='LCURLY: '{'RCURLY: '}'LBRACKET: '['RBRACKET: ']'DOUBLE_LBRACKET: '[['ELLIPSIS: '...'RANGE: '..' 等,直接对应 ASN.1 的符号。

字面量(字符串/位串/十六进制串)

  • BSTRING: APOSTROPHE ('0'..'1')* APOSTROPHE 'B' —— 二进制串,如 '1010'B
  • HSTRING: APOSTROPHE ('0'..'9'|'a'..'f'|'A'..'F')* APOSTROPHE 'H' —— 十六进制串。
  • CSTRING: '"' ( EscapeSequence | ~('\\'|'"') )* '"' —— 加引号的字符串,支持转义(如 \n),用于 PATTERN 或值中。

标识符(ASN.1 引用名称)

  • UCASE_ID:大写字母开头,后跟字母、数字或连字符(-),但连字符后必须跟字母数字(标准要求)。用于类型引用、模块名等。
  • LCASE_ID:小写字母开头,规则相同。用于值引用、组件名等。
  • 这种定义允许 ASN.1 中的连字符名称(如 My-Type),但要避免与减号混淆(减号是 MINUS 单独 Token)。

数字

  • NUMBER:非负整数,不含前导零(除了单独的 0)。
  • NUMBER_WITH_DECIMALS:带小数点的实数(如 1.23),用于 REAL 类型的值。

注释

  • LINE_COMMENT-- 到行尾 → skip
  • BLOCK_COMMENT/* ... */skip,支持多行。
  • 注释完全忽略,不影响解析。

空白

  • WS: (' '|'\r'|'\t'|'\n') -> skip

备用捕获(OTHER)

  • OTHER: . —— 捕获任意未匹配字符(通常用于错误恢复,但会匹配所有剩余,若放在最后可作为“万能”回退,但此处放在文件末尾,优先匹配其他规则,实际上不会匹配到,因为其他规则会覆盖;若无其他规则匹配,则会报错。此处可能多余,但不影响)。

语法规则(Parser Rules)核心剖析

语法规则定义了 ASN.1 的层次结构。按“模块→类型→值→约束”这条主线来梳理。

顶层:specification

specification: moduleDefinition* ;

一个 ASN.1 文件可由多个模块定义组成(允许为空)。

模块定义:moduleDefinition

moduleDefinition:  
moduleIdentifier
DEFINITIONS_LITERAL
tagDefault?
extensionDefault?
ASSIGN
BEGIN_LITERAL
( exports? imports? assignment assignment* )?
END_LITERAL
;

模拟标准中的模块语法:

  • moduleIdentifier:模块名 + 可选的 OID(如 {iso(1) ...})。
  • DEFINITIONS 关键字。
  • tagDefaultEXPLICIT TAGS / IMPLICIT TAGS / AUTOMATIC TAGS(可选)。
  • extensionDefaultEXTENSIBILITY IMPLIED(可选)。
  • ::=BEGIN
  • 模块体:可选的 exports(导出)、imports(导入),然后是一个或多个 assignment(类型或值赋值)。
  • END

其中 assignment 又分为 typeAssignmentvalueAssignment,省略了其他如对象类赋值(注释中有说明)。

类型定义(Type Assignment)

typeAssignment:
( UCASE_ID | characterStringType ) // 类型引用或内建类型重定义
ASSIGN
type
;

允许为内建字符串类型起别名(如 MyString ::= IA5String)。

类型表达式:type

type: builtinType constraint? | referencedType constraint? ;

一个类型可以是内建类型(如 INTEGERSEQUENCE)、引用类型(如已定义的类型名、选择类型、有用类型),并可附加约束(如 INTEGER(0..10))。

内建类型 builtinType

包含所有 ASN.1 核心类型:

  • bitStringTypeBIT STRING { a(0), b(1) }
  • BOOLEAN_LITERALNULL_LITERAL
  • characterStringType:各种字符串(IA5StringUTF8String 等)。
  • choiceTypeenumeratedTypeintegerTypesequenceTypesetTypesequenceOfTypesetOfTypeprefixedType(带标签)等。
  • 还有 anyTypeANY DEFINED BY)等扩展。

每个类型的具体规则都严格按照标准定义,例如:

  • SequenceType

    sequenceType:
    SEQUENCE_LITERAL LCURLY componentTypeLists? RCURLY
    ;

    其中 componentTypeLists 处理带有扩展标记(...)、异常、可选/默认组件的复杂情况。

  • ChoiceType

    choiceType:
    CHOICE_LITERAL LCURLY alternativeTypeList ( COMMA extensionAndException extensionAdditionAlternatives? extensionMarker? )? RCURLY
    ;

    允许在备选列表后跟扩展。

引用类型 referencedType

包括:

  • usefulTypeGeneralizedTimeUTCTimeObjectDescriptor
  • definedType:通过 UCASE_IDModule.Type 引用。
  • selectionType:如 name < MyType(选择类型)。

标签类型 taggedType

taggedType:
LBRACKET
( UCASE_ID COLON )? // 编码引用(可选)
( UNIVERSAL_LITERAL | APPLICATION_LITERAL | PRIVATE_LITERAL )? // 类
( NUMBER | LCASE_ID ) // 标签号
RBRACKET
( IMPLICIT_LITERAL | EXPLICIT_LITERAL )?
type
;

支持隐式/显式标签,以及上下文、应用、私有等标签类。

值定义(Value Assignment)

valueAssignment:
LCASE_ID
type
ASSIGN
value
;

值名称(小写开头) + 类型 + ::= + 值表达式。

值表达式 value 涵盖多种形式,用备选分支标签(#)区分:

  • value_CSTRING:带引号字符串。
  • value_BSTRING / value_HSTRING:二进制/十六进制串。
  • value_EmptyList:空列表 {}
  • value_BOOLEANTRUE / FALSE
  • value_Choice:选择类型值,如 name: value
  • value_NamedValueList:命名值列表(用于 SEQUENCE/SET)。
  • value_ObjectIdentifier:OID 如 {iso(1) 2 3}
  • value_ValueList:无名字的值列表(用于 SEQUENCE OF / SET OF)。
  • value_Integer:整数或小数。
  • value_NULLNULL
  • value_ReferencedValue:引用已定义的值。

约束(Constraint)

constraint:
LPAREN
subtypeConstraint
RPAREN
;

约束总是括在括号中。subtypeConstraint 支持:

  • 集合运算:|(UNION)、^(INTERSECTION)、EXCEPT(排除)。
  • 元素列表:值、值范围(1..10)、SIZEFROMPATTERNWITH COMPONENT 等。
  • 允许使用 MINMAX 表示端点。
  • 扩展标记 ... 用于表示未来扩展。

约束规则中使用了很多递归,如 unionsintersectionsintersectionElementselements,以正确解析运算符优先级。


关键设计模式与 ANTLR4 特性应用

备选分支标签(Alternative Labels)

value 规则中广泛使用,如:

value: CSTRING # value_CSTRING
| BSTRING # value_BSTRING
...

这样在生成的监听器/访问器中,会为每个分支生成独立的 enterValue_CSTRING / exitValue_CSTRING 等方法,方便精细化处理,而不用在统一的 enterValue 里通过 instanceof 判断。

使用 fragment 组织词法片段

EscapeSequence 被定义为 fragment,供 CSTRING 复用,避免重复。

规则优先级与歧义处理

  • componentTypeLists 中,通过多个备选分支(用 # 标记)和顺序排列,确保扩展标记 ... 与普通组件组合时解析正确。
  • type 中,builtinTypereferencedType 并列,ANTLR4 会按顺序尝试,但有些输入可能同时匹配两者(例如 INTEGER 既是内建类型也可能是引用?但 INTEGER 是关键字,词法上已被识别为 INTEGER_LITERAL,因此不会冲突)。若存在歧义,ANTLR4 采用“优先第一条匹配”策略,此语法已谨慎处理。

大量使用 ?*+ 描述可选和重复

例如 moduleIdentifier( definitiveOID )?componentTypeList( COMMA componentType )*,这都是 ANTLR4 的常用操作符。

异常处理和扩展标记(...!

ASN.1 支持扩展标记(...)和异常标识(!),本语法在 componentTypeListsenumerationextensionAddition 等规则中均有体现,允许解析带有扩展的模块定义。


对 ASN.1 标准的覆盖程度(及简化点)

注释中明确提到简化了以下部分:

  • 参数化引用(Parameterized references)未实现(仅保留基础引用)。
  • 对象类(ObjectClass)、对象(Object)、对象集(ObjectSet)等 X.681 相关的高级特性未实现。
  • 编码控制段(Encoding Control Sections)忽略。
  • XML 值等未包含。

但核心的类型、值、约束、模块、导入导出、标签、扩展等全部支持,足以解析绝大多数 ASN.1 规范(如 SNMP MIB、LDAP、3GPP 等标准)。


如何使用此语法生成解析器并测试

  1. 生成代码

    • 使用 ANTLR4 工具(或 IDE 插件)执行 antlr4 ASN.g4,生成词法分析器 ASNLexer.java、语法分析器 ASNParser.java 以及监听器骨架等。
    • 若需要访问器(Visitor),可加 -visitor 选项。
  2. 入口规则:从 specification 开始解析(也可从 moduleDefinition 开始解析单个模块)。

  3. Java 示例

    CharStream input = CharStreams.fromFileName("myModule.asn");
    ASNLexer lexer = new ASNLexer(input);
    CommonTokenStream tokens = new CommonTokenStream(lexer);
    ASNParser parser = new ASNParser(tokens);
    ParseTree tree = parser.specification(); // 解析整个文件
    // 然后可用监听器或访问器遍历 tree
  4. 测试工具:可以用 grun(TestRig)图形化查看语法树,例如:

    grun ASN specification -gui myModule.asn

ASN.g4 这个语法文件中,ANTLR4 的原生关键字主要用于定义语法文件的结构控制生成的解析器行为。它不是用来匹配输入文本(如 BEGININTEGER)的,那些是在词法规则里定义的自定义Token。

以下是文件中出现的 ANTLR4 原生关键字及其详细说明。

核心结构关键字

这类关键字定义了 .g4 文件的骨架。

  • grammar: 必须出现在文件开头,用于声明语法名称。例如文件中的 grammar ASN;,它要求文件名必须为 ASN.g4
  • lexer: 用于声明一个纯词法分析器语法,其中只包含词法规则,没有解析器规则。
  • parser: 用于声明一个纯语法分析器语法,其中只包含解析器规则,没有词法规则。
  • fragment: 用于定义一个可复用的词法片段fragment 规则本身不会产生独立的Token,只能被其他词法规则引用。在 ASN.g4 中,EscapeSequence 就是一个典型的 fragment 规则,它被用在 CSTRING 词法规则里。
  • mode: 用于定义词法分析器的不同状态(模式)。在 ASN.g4 中没有使用,它通常在需要根据上下文切换词法规则的复杂场景下使用(比如解析HTML或XML)。

选项与配置关键字

这类关键字用于配置语法分析器的行为。

  • options: 用于在语法文件内部设置各种选项。例如,可以设置 tokenVocab 选项来指定词法Token的类型。在 ASN.g4 中没有使用 options {} 块。
  • tokens: 用于声明虚构的Token类型。这些Token在语法规则中被引用,但实际上并没有对应的词法规则来生成它,通常用于在解析器生成时定义操作符优先级等。在 ASN.g4 中没有使用。

动作与代码注入关键字

这类关键字用于在生成的解析器中嵌入自定义代码。

  • returns: 用于为解析器规则定义返回值。例如,一个计算器语法中的 expr returns [int value]。在 ASN.g4 中没有使用。
  • locals: 用于为解析器规则定义局部变量。这些变量可以在规则内部使用。在 ASN.g4 中没有使用。
  • throws: 用于声明解析器规则可能抛出的异常。在 ASN.g4 中没有使用。
  • catchfinally: 用于在生成的解析器代码中添加异常处理和最终执行块。在 ASN.g4 中没有使用。
  • import: 用于在当前语法文件中导入其他语法文件。被导入的语法中的规则和Token定义会被合并进来。在 ASN.g4 中没有使用。

一些重要的区分

  1. 关键字 vs. 自定义TokenASN.g4 文件中定义了大量的Token,比如 BEGIN_LITERALINTEGER_LITERAL 等,它的值是 'BEGIN''INTEGER' 这样的字符串。这些是在为ASN.1语言本身定义的Token,它不是 ANTLR4 的原生关键字。
  2. rule 不是关键字:需要注意的是,尽管 rule 这个词很常用,但它不是 ANTLR4 的保留关键字。不过,官方建议不要将其用作规则名称,以免造成混淆。

ASN.g4 这个文件中,实际使用的 ANTLR4 原生关键字非常少,主要是:

  • grammar: 用于声明语法名称。
  • fragment: 用于定义可复用的词法片段。

其他原生关键字(如 import, options, returns 等)虽然功能强大,但这个特定的语法文件并未采用。